/* 
 * Copyright 2011 VelocitiSoftware
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.velociti.ikarus.widget.client.ui;

import java.util.Iterator;

import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.terminal.gwt.client.ApplicationConnection;
import com.vaadin.terminal.gwt.client.Paintable;
import com.vaadin.terminal.gwt.client.UIDL;
import com.vaadin.terminal.gwt.client.ui.Field;

/* ######VIkarusBTextField#####
 * @author Alper Turkyilmaz - VelocitiSoftware - 2011
 * @version 1.0.1
 */

public class VIkarusBTextField extends ScrollPanel implements Paintable, Field {
	private static final String CLASSNAME = "v-ikarusbtextfield";

	// line height
	private static int CONST_HEIGHT = 22;

	// Average character length
	private static int CONST_CHAR_LENGTH = 6;

	// horizontal padding between buttons, should be taken from css style..
	private static int CONST_HORZ_PADDING = 5;

	// Gap for the next line
	private static int CRITICAL_WIDTH = 20;

	public static final int BACKSPACE = KeyCodes.KEY_BACKSPACE;
	public static final int ARROW_LEFT = KeyCodes.KEY_LEFT;
	public static final int ARROW_RIGHT = KeyCodes.KEY_RIGHT;
	public static final int DELETE = KeyCodes.KEY_DELETE;

	private TextBox tf;
	private Line currentLine;
	protected boolean immediate;
	protected String id;
	protected ApplicationConnection client;
	protected char textDelimiter = ' ';
	private String valueBeforeEdit = "";
	private int nextFocusedLineIndex = -1;
	private int nextFocusedWidgetIndex = -1;
	private boolean addingRequiredNewLine = false;
	private int maxLinesToExpand = 3;

	private VerticalPanel vPanel;

	public VIkarusBTextField() {
		init();
	}

	class Line extends HorizontalPanel {
		int usedWidth = 0;

		void setUsedWidth(int buttonLength, boolean add) {
			int length = buttonLength + CONST_HORZ_PADDING;
			usedWidth += add ? length : -length;
		}

		public int getUsedWidth() {
			return usedWidth;
		}

		int getUnusedWidth(int textFieldLength) {
			return getBody().getClientWidth() - (usedWidth + textFieldLength);
		}

		public Line() {
			init();
		}

		public void init() {
			this.setStyleName(CLASSNAME + "-line");
			this.setHeight(CONST_HEIGHT + "px");
			this.setHorizontalAlignment(ALIGN_LEFT);
			this.setVerticalAlignment(ALIGN_MIDDLE);
		}

		Button addButton(String buttonCaption) {
			Button b = new Button();
			b.setText(buttonCaption);
			b.setStyleName(CLASSNAME + "-button");
			// if (clickListener != null) {
			// b.addListener(clickListener);
			// }
			b.addFocusHandler(new FocusHandler() {
				public void onFocus(FocusEvent event) {
					setCurrentLine((Line) ((Button) event.getSource()).getParent());
					VIkarusBTextField.this.removeStyleName(CLASSNAME);
					VIkarusBTextField.this.addStyleName(CLASSNAME + "-focus");
					nextFocusedLineIndex = VIkarusBTextField.this.vPanel.getWidgetIndex(currentLine);
					nextFocusedWidgetIndex = getWidgetIndex(((Button) event.getSource()));
				}
			});
			b.addBlurHandler(new BlurHandler() {
				public void onBlur(BlurEvent event) {
					VIkarusBTextField.this.removeStyleName(CLASSNAME + "-focus");
					VIkarusBTextField.this.addStyleName(CLASSNAME);
				}
			});
			b.addKeyDownHandler(new KeyDownHandler() {
				public void onKeyDown(KeyDownEvent event) {
					handleKeyboardEvent(event);
				}
			});

			int index = getWidgetIndex(tf);
			insertComponent(b, index);
			setUsedWidth(b.getOffsetWidth(), true);
			return b;
		}

		void addComponent(Widget widget) {
			add(widget);
			layout();
		}

		void layout() {
			int count = getWidgetCount();
			if (count > 0) {
				int summedwidth = 0;
				for (int i = 0; i < count - 1; i++) {
					Widget w = getWidget(i);
					setCellWidth(w, (w.getOffsetWidth() + CONST_HORZ_PADDING) + "px");
					summedwidth += (w.getOffsetWidth() + CONST_HORZ_PADDING);
				}
				int totalWidth = getBody().getClientWidth();
				Widget w = getWidget(count - 1);
				setCellWidth(w, (totalWidth - summedwidth) + "px");
			}
		}

		void insertComponent(Widget widget, int index) {
			insert(widget, index);
			layout();
		}

		void removeComponent(Widget widget) {
			remove(widget);
			layout();
		}

		FocusWidget getWidget(Element element) {
			int widgetCount = getWidgetCount();
			FocusWidget fWidget = null;
			for (int i = 0; i < widgetCount; i++) {
				FocusWidget widget = (FocusWidget) getWidget(i);
				if (element.equals(widget.getElement())) {
					fWidget = widget;
					break;
				}
			}
			return fWidget;
		}
	}

	int getTextFieldLength() {
		return tf.getText().length() * CONST_CHAR_LENGTH;
	}

	public void clearText() {
		vPanel.clear();
		addLineAndSetAsCurrent();
		tf = new TextBox();
		tf.setStyleName(CLASSNAME + "-text");
		tf.setWidth("100%");
		currentLine.add(tf);
		tf.addFocusHandler(new FocusHandler() {
			public void onFocus(FocusEvent event) {
				setCurrentLine((Line) tf.getParent());
				removeStyleName(CLASSNAME);
				addStyleName(CLASSNAME + "-focus");
				if (!addingRequiredNewLine) {
					nextFocusedLineIndex = vPanel.getWidgetIndex(currentLine);
					nextFocusedWidgetIndex = currentLine.getWidgetIndex(tf);
				}
			}
		});
		tf.addBlurHandler(new BlurHandler() {
			public void onBlur(BlurEvent event) {
				removeStyleName(CLASSNAME + "-focus");
				addStyleName(CLASSNAME);
				textValueChange();
			}
		});
		tf.addKeyDownHandler(new KeyDownHandler() {
			public void onKeyDown(KeyDownEvent event) {
				handleKeyboardEvent(event);
			}
		});
		tf.addKeyPressHandler(new KeyPressHandler() {
			public void onKeyPress(KeyPressEvent event) {
				String text = (String) tf.getValue();
				int cursorposition = tf.getCursorPos();
				addNewLineIfNeeded(getTextFieldLength());
				String buttonCaption = text.substring(0, cursorposition);
				if (event.getCharCode() == textDelimiter) {
					buttonCaption = buttonCaption.substring(0, cursorposition);
					Button button = currentLine.addButton(buttonCaption);
					if (text.length() > cursorposition) {
						text = text.substring(cursorposition);
					} else {
						text = "";
					}
					tf.setText(text);
					nextFocusedLineIndex = vPanel.getWidgetIndex(currentLine);
					nextFocusedWidgetIndex = currentLine.getWidgetIndex(button) + 1;
					itemSetChange(button.getText(), true);
				}
			}
		});
	}

	Line addLine() {
		Line line = new Line();
		line.setHorizontalAlignment(HorizontalPanel.ALIGN_LEFT);
		line.setVerticalAlignment(HorizontalPanel.ALIGN_MIDDLE);
		line.setWidth("100%");
		vPanel.add(line);
		recalculateHeigth();
		return line;
	}

	void recalculateHeigth() {
		int newHeigth = vPanel.getWidgetCount() * CONST_HEIGHT;
		vPanel.setHeight(newHeigth + "px");
		if (getElement().getOffsetHeight() < newHeigth && vPanel.getWidgetCount() < maxLinesToExpand + 1) {
			this.setHeight(newHeigth+1 + "px");
		}
	}

	Line addLineAndSetAsCurrent() {
		Line newLine = addLine();
		setCurrentLine(newLine);
		return newLine;
	}

	void deleteLine(Line line) {
		if (line.equals(vPanel.getWidget(0))) {
			return;
		} else {
			vPanel.remove(line);
			recalculateHeigth();
		}
	}

	void setCurrentLine(Line line) {
		currentLine = line;
	}

	public void init() {
		setStyleName(CLASSNAME);
		vPanel = new VerticalPanel();
		vPanel.setWidth("100%");
		vPanel.setHorizontalAlignment(VerticalPanel.ALIGN_LEFT);
		vPanel.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE);
		setWidget(vPanel);
		clearText();
	}

	void addNewLineIfNeeded(int textFieldLength) {
		if (currentLine.getUnusedWidth(textFieldLength) <= CRITICAL_WIDTH) {
			try {
				addingRequiredNewLine = true;
				int index = currentLine.getWidgetIndex(tf);
				currentLine.removeComponent(tf);
				addLineAndSetAsCurrent();
				currentLine.addComponent(tf);
				tf.setFocus(true);
			} finally {
				addingRequiredNewLine = false;
			}
		}
	}

	public void handleKeyboardEvent(KeyDownEvent event) {
		final int keyCode = event.getNativeKeyCode();
		if (keyCode != BACKSPACE && keyCode != DELETE && keyCode != ARROW_LEFT && keyCode != ARROW_RIGHT) {
			return;
		}
		FocusWidget target = (FocusWidget) event.getSource();
		if (target instanceof TextBox) {
			TextBox textField = (TextBox) target;
			int cursorposition = textField.getCursorPos();
			String text = (String) textField.getValue();
			int widgetIndex = currentLine.getWidgetIndex((Widget) target);
			int currentLineIndex = vPanel.getWidgetIndex(currentLine);
			if (keyCode == BACKSPACE || keyCode == ARROW_LEFT) {
				if (cursorposition <= 0) {
					if (widgetIndex > 0) {
						Button button = (Button) currentLine.getWidget(widgetIndex - 1);
						button.setFocus(true);
					} else if (currentLineIndex != 0) {
						Line prevLine = (Line) vPanel.getWidget(currentLineIndex - 1);
						int last = prevLine.getWidgetCount();
						((FocusWidget) prevLine.getWidget(last - 1)).setFocus(true);
					}
				}
			}
		} else if (target instanceof Button) {
			Button targetButton = (Button) target;
			int widgetIndex = currentLine.getWidgetIndex(targetButton);
			int currentLineIndex = vPanel.getWidgetIndex(currentLine);
			if (keyCode == BACKSPACE) {// BACKSPACE

				currentLine.setUsedWidth(targetButton.getOffsetWidth(), false);
				currentLine.removeComponent(targetButton);
				if (vPanel.getWidgetCount() - 1 > currentLineIndex) {
					Line nextLine = (Line) vPanel.getWidget(currentLineIndex + 1);
					FocusWidget comp = (FocusWidget) nextLine.getWidget(0);
					int length = 0;
					if (comp instanceof Button) {
						length = (int) comp.getOffsetWidth();
					} else {
						length = getTextFieldLength();
					}
					if (currentLine.getUnusedWidth(getTextFieldLength()) > length) {
						nextLine.removeComponent(comp);
						if (comp instanceof Button) {
							nextLine.setUsedWidth(length, false);
							currentLine.setUsedWidth(length, true);
						}
						currentLine.addComponent(comp);
						if (nextLine.getWidgetCount() == 0) {
							deleteLine(nextLine);
						}
					}

				}
				// keep values to focus after server update
				if (widgetIndex > 0) {
					nextFocusedLineIndex = vPanel.getWidgetIndex(currentLine);
					nextFocusedWidgetIndex = widgetIndex - 1;
				} else if (currentLineIndex != 0) {
					Line prevLine = (Line) vPanel.getWidget(currentLineIndex - 1);
					nextFocusedLineIndex = vPanel.getWidgetIndex(prevLine);
					nextFocusedWidgetIndex = prevLine.getWidgetCount() - 1;
				}
				itemSetChange(targetButton.getText(), false);
			} else if (keyCode == DELETE) {// DELETE
				currentLine.setUsedWidth(targetButton.getOffsetWidth(), false);
				currentLine.removeComponent(targetButton);
				if (vPanel.getWidgetCount() - 1 > currentLineIndex) {
					Line nextLine = (Line) vPanel.getWidget(currentLineIndex + 1);
					FocusWidget comp = (FocusWidget) nextLine.getWidget(0);
					int length = 0;
					if (comp instanceof Button) {
						length = (int) comp.getOffsetWidth();
					} else {
						length = ((String) ((TextBox) comp).getValue()).length() * CONST_CHAR_LENGTH;
					}
					if (currentLine.getUnusedWidth(getTextFieldLength()) > length) {
						nextLine.removeComponent(comp);
						if (comp instanceof Button) {
							nextLine.setUsedWidth(length, false);
							currentLine.setUsedWidth(length, true);
						}
						currentLine.addComponent(comp);
						if (nextLine.getWidgetCount() == 0) {
							deleteLine(nextLine);
						}
					}
				}
				// keep values to focus after server update
				if (currentLine.getWidgetCount() > widgetIndex) {
					nextFocusedLineIndex = vPanel.getWidgetIndex(currentLine);
					nextFocusedWidgetIndex = widgetIndex;
				}
				itemSetChange(targetButton.getText(), false);
			} else if (keyCode == ARROW_LEFT) {// BACKWARD
				if (widgetIndex > 0) {
					Button button = (Button) currentLine.getWidget(widgetIndex - 1);
					button.setFocus(true);
				} else if (currentLineIndex != 0) {
					Line prevLine = (Line) vPanel.getWidget(currentLineIndex - 1);
					int last = prevLine.getWidgetCount();
					((FocusWidget) prevLine.getWidget(last - 1)).setFocus(true);
				}
			} else if (keyCode == ARROW_RIGHT) {// FORWARD
				// NAVIGATION
				if (currentLine.getWidgetCount() > widgetIndex + 1) {
					((FocusWidget) currentLine.getWidget(widgetIndex + 1)).setFocus(true);
				} else if (vPanel.getWidgetCount() - 1 > currentLineIndex) {
					Line nextLine = (Line) vPanel.getWidget(currentLineIndex + 1);
					((FocusWidget) nextLine.getWidget(0)).setFocus(true);
				}
			}
		}
	}

	public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
		this.client = client;
		id = uidl.getId();
		if (client.updateComponent(this, uidl, true)) {
			return;
		}
		if (uidl.hasAttribute("immediate")) {
			immediate = uidl.getBooleanAttribute("immediate");
		}
		if (uidl.hasAttribute("maxLinesToExpand")) {
			maxLinesToExpand = uidl.getIntVariable("maxLinesToExpand");
		}

		clearText();
		if (uidl.hasVariable("textValue")) {
			valueBeforeEdit = uidl.getStringVariable("textValue");
			String text = valueBeforeEdit;
			tf.setValue(text);
		}
		if (uidl.hasVariable("textDelimiter")) {
			textDelimiter = (char) uidl.getIntVariable("textDelimiter");
		}
		buildButtons(uidl);

		// focus to the next widget kept in memory
		FocusWidget widget = tf;
		if (nextFocusedLineIndex != -1 && nextFocusedWidgetIndex != -1) {
			if (vPanel.getWidgetCount() - 1 < nextFocusedLineIndex) {
				nextFocusedLineIndex = vPanel.getWidgetCount() - 1;
				Line line = (Line) vPanel.getWidget(nextFocusedLineIndex);
				nextFocusedWidgetIndex = line.getWidgetCount() - 1;
				widget = (FocusWidget) line.getWidget(nextFocusedWidgetIndex);
			} else {
				Line line = (Line) vPanel.getWidget(nextFocusedLineIndex);
				widget = (FocusWidget) line.getWidget(nextFocusedWidgetIndex);
			}
		}
		widget.setFocus(true);
	}

	public void textValueChange() {
		String newText = tf.getValue();
		if (client != null && id != null && !valueBeforeEdit.equals(newText)) {
			client.updateVariable(id, "textValue", newText, false);
			valueBeforeEdit = newText;
			if (immediate) {
				client.sendPendingVariableChanges();
			}
		}
	}

	public void itemSetChange(String itemId, boolean add) {
		if (client != null && id != null) {
			client.updateVariable(id, add ? "itemAdded" : "itemRemoved", true, false);
			client.updateVariable(id, "itemId", itemId, false);
			if (immediate) {
				client.sendPendingVariableChanges();
			}
		}
	}

	protected void buildButtons(UIDL uidl) {
		for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) {
			final UIDL childUidl = (UIDL) i.next();
			if (childUidl.getTag().equals("buttons")) {
				for (Iterator iterator = childUidl.getChildIterator(); iterator.hasNext();) {
					UIDL so = (UIDL) iterator.next();
					String buttonText = so.getStringAttribute("key");
					addNewLineIfNeeded(buttonText.length() * CONST_CHAR_LENGTH);
					currentLine.addButton(buttonText);
				}
			}
		}
	}
}
